thiserror & anyhow
| Crate | Best for | Error type style |
|---|---|---|
thiserror | Libraries | Strongly typed, structured errors |
anyhow | Applications | Flexible, dynamic, context-rich errors |
Think of it like this:
- Libraries → use
thiserror - Applications / binaries → use
anyhow
thiserror — Ergonomic custom error types
Writing custom error types manually requires:
- Implementing
Display - Implementing
Error - Implementing
Fromfor wrapped errors
thiserror generates all of that for you using derive macros.
Example: Library-style error with thiserror
use thiserror::Error;
use std::io;
use std::num::ParseIntError;
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("I/O error while reading config: {0}")]
Io(#[from] io::Error),
#[error("Invalid number in config: {0}")]
Parse(#[from] ParseIntError),
#[error("Missing required field: {0}")]
MissingField(String),
}
pub fn read_config_number(path: &str) -> Result<i32, ConfigError> {
let contents = std::fs::read_to_string(path)?;
let number = contents.trim().parse::<i32>()?;
Ok(number)
}
#[derive(Error)]automatically:- Implements
std::error::Error - Implements
Displayusing#[error(...)] - Implements
Fromfor variants marked with#[from]
- Implements
- Callers can match on
ConfigErrorvariants.
anyhow — Flexible error handling for applications
In applications:
- You often don’t care exactly which error type occurred.
- You want:
- Easy error propagation
- Rich context
- Good backtraces
- Minimal boilerplate
anyhow provides a single error type: anyhow::Error.
Example: Application-style error handling with anyhow
use anyhow::{Context, Result};
fn read_number(path: &str) -> Result<i32> {
let contents = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read file '{}'", path))?;
let number = contents
.trim()
.parse::<i32>()
.context("Failed to parse number from file")?;
Ok(number)
}
fn main() -> Result<()> {
let n = read_number("number.txt")?;
println!("Number: {}", n);
Ok(())
}
Result<T>is shorthand forResult<T, anyhow::Error>..context(...)and.with_context(...)add helpful messages.- Errors automatically carry:
- The original cause
- A context chain
- A backtrace (when enabled)
Using thiserror + anyhow together
This is very common:
- Library code uses
thiserror. - Application code uses
anyhowand converts library errors automatically.
Example: Library + App integration
Library crate:
use thiserror::Error;
use std::io;
use std::num::ParseIntError;
#[derive(Debug, Error)]
pub enum DataError {
#[error("I/O error: {0}")]
Io(#[from] io::Error),
#[error("Parse error: {0}")]
Parse(#[from] ParseIntError),
}
pub fn read_data(path: &str) -> Result<i32, DataError> {
let contents = std::fs::read_to_string(path)?;
let number = contents.trim().parse::<i32>()?;
Ok(number)
}
Application crate:
use anyhow::Result;
use mylib::read_data;
fn main() -> Result<()> {
let value = read_data("data.txt")?;
println!("Value: {}", value);
Ok(())
}
DataErrorautomatically converts intoanyhow::Error.- You get structured errors in the library and flexible handling in the app.
Key Differences
| Feature | thiserror | anyhow |
|---|---|---|
| Error type | Your own enum/struct | anyhow::Error |
| Best for | Libraries | Applications |
| Error matching | Yes (match on variants) | No (opaque type) |
| Context messages | Manual | Built-in (context, with_context) |
| Backtraces | Via std / feature | Built-in support |
| Boilerplate | Low | Very low |
When not to use anyhow
Avoid anyhow in:
- Public library APIs
- Code where callers must distinguish error types
- Low-level libraries
Use thiserror instead.
Summary
thiserror:- Use for defining clean, typed, structured error enums.
- Ideal for libraries and reusable modules.
anyhow:- Use for application-level error handling.
- Provides easy propagation, context, and backtraces.
- Together, they give you:
- Strong typing internally
- Flexible error handling at the top level